package com.pattern.structural.proxy;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Before;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @Description using static proxy to cache querying.
 * @Author Ping
 * @Date 2022/4/4 15:29
 **/
public class ProxyDemo {

    public static void main(String args[]) {
        testStaticProxy();
//        testDynamicJDKProxy() ;
//        testDynamicJDKProxyForCaching() ;
//
    }
    public static void testStaticProxy() {
        UserServiceImpl userService = new UserServiceImpl() ;
        /**
         * binding the proxy relation.
         */
        UserService userServiceProxy = new UserServiceCacheProxy(userService) ;
        System.out.println(userServiceProxy.getUserById(1)) ;
        System.out.println(userServiceProxy.getUserById(1)) ;
    }
    public static void testDynamicJDKProxy() {
        UserService userService = new UserServiceImpl() ;
        UserService userDynamicProxy = (UserService) MyJdkProxyFactory.getProxy(userService) ;
        userDynamicProxy.getUserById(1) ;
        ExampleInterface exampleProxy = (ExampleInterface) MyJdkProxyFactory.getProxy(new ExampleInterface() {
            @Override
            public int add(int a, int b) {
                System.out.println("example obj was working.") ;
                return a + b ;
            }
        }) ;
        System.out.println(exampleProxy.add(1,2)) ;

//        User userProxy = (User) MyJdkProxyFactory.getProxy(new User()) ;
//        userProxy.doWork();

    }

    public static void testDynamicJDKProxyForCaching() {
        UserService userService = new UserServiceImpl();
        UserService userDynamicProxy = (UserService) MyJdkProxyFactory.getProxy(userService,
                                                               new GetCachingBeforeAdvice(), new DoCachingAfterAdvice());
        userDynamicProxy.getUserById(1);
        userDynamicProxy.getUserById(1);
    }

}

class User {
    int id ;
    String name ;
    public User() {
        this.id = new Random().nextInt(100) ;
        this.name = "user_" + this.id ;
    }
    public User(int id) {
        this.id = id;
        this.name = "user_" + this.id ;
    }

    public void doWork() {
        System.out.println("user do work.") ;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

interface UserService {
    public User getUserById(int id ) ;
}

interface ExampleInterface {
    public int add(int a, int b) ;
}

class UserServiceImpl implements  UserService {
    @Override
    public User getUserById(int id) {
        System.out.println("Query user from DB.");
        return new User(id) ;
    }
}
/**
 * Using static proxy to cache user by id
 */

class UserServiceCacheProxy implements UserService {
    /**
     * the target object for proxy
     */
    UserServiceImpl targetUserService ;
    /**
     * for caching queried user.
     */
    Map<Integer, User> userCache ;

    public UserServiceCacheProxy(UserServiceImpl targetUserService) {
        this.targetUserService = targetUserService;
        this.userCache = new ConcurrentHashMap<>() ;
    }

    @Override
    public User getUserById(int id) {
    /**
     * various proxy logic (enhancing): caching, permission control, virtual obj, etc.
     */
        if (this.userCache.containsKey(id)) {
            System.out.println("Get from caching.") ;
            return this.userCache.get(id) ;
        }
        /**
         *  target method
         */
        User user = this.targetUserService.getUserById(id ) ;
        this.userCache.put(id, user) ;
        return user;
    }
}

/**
 * a factory to build proxy obj for target obj by JDK reflect (CGLib).
 * Key point:
 *  1. target obj must have at least one interface, since there was a interface to be implemented
 *          by dynamic proxy class .
 *   2. invocation Handler: enhancing logic was written over here.
 *
 */
class MyJdkProxyFactory {

    public static Object getProxy(Object target) {
        // to get the proxy by Proxy from JDK reflection
        // classloader
        ClassLoader loader = MyJdkProxyFactory.class.getClassLoader() ;
        // interfaces:
        Class[] interfaces = target.getClass().getInterfaces() ;
        // invocation handler.
        InvocationHandler handler = new MyJdkDynamicProxy(target) ;
        return Proxy.newProxyInstance(loader, interfaces, handler) ;
    }
    public static Object getProxy(Object target, BeforeAdv beforeAdv, AfterAdv afterAdv) {
        // to get the proxy by Proxy from JDK reflection
        // classloader
        ClassLoader loader = MyJdkProxyFactory.class.getClassLoader() ;
        // interfaces:
        Class[] interfaces = target.getClass().getInterfaces() ;
        // invocation handler.
        InvocationHandler handler = new MyJdkDynamicProxy(target, beforeAdv, afterAdv) ;
        return Proxy.newProxyInstance(loader, interfaces, handler) ;
    }

}
class GetCachingBeforeAdvice implements BeforeAdv {
    static Map<Integer, Object> caching = new ConcurrentHashMap<>() ;
    @Override
    public Object before(Object[] args) {
        if (args == null) return null;
        /**
         * in default, the first parameter was key.
         */
        int key = (Integer) args[0] ;
        if (caching.containsKey(key)) {
            System.out.println("Get value from caching for key :  " + key) ;
            return caching.get(key) ;
        }
        return null;
    }
}

class DoCachingAfterAdvice implements AfterAdv {
    @Override
    public Object after(Object[] args) {
        /**
         * do caching. Assuming that, the first parameter was intger key and value is object.
         */
        if (args == null) return null ;
        System.out.println("caching for key :  " + (Integer)args[0]) ;
        GetCachingBeforeAdvice.caching.put((Integer)args[0], args[1]) ;
        return null ;
    }
}

/**
 * Before advice
 */
interface BeforeAdv {
    Object before(Object[] args) ;
}

/**
 * after advice
 */
interface AfterAdv {
    Object after(Object... args) ;
}

class MyJdkDynamicProxy implements InvocationHandler {

    Object targetObj ;
    BeforeAdv beforeAdvice ;
    AfterAdv afterAdvice ;

    public MyJdkDynamicProxy(Object targetObj) {
        this.targetObj = targetObj;
    }

    public MyJdkDynamicProxy(Object targetObj, BeforeAdv beforeAdvice, AfterAdv afterAdvice) {
        this.targetObj = targetObj;
        this.beforeAdvice = beforeAdvice;
        this.afterAdvice = afterAdvice;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        /**
         * Here, before logic could be added here.
         */
        Object ret = null ;
        if (beforeAdvice != null ) {
            ret = beforeAdvice.before(args);
            if (ret != null) {
                return ret ;
            }
        }
        ret = method.invoke(targetObj, args) ;
        /**
         * after logic could be added here.
         */
        if (afterAdvice != null) {
            afterAdvice.after(args[0], ret);
        }
        return ret;
    }
}







